package com.btcode.netty.server;

import com.btcode.common.MyLog;
import com.btcode.log.ILog;
import com.btcode.netty.server.handler.AbstractHrtDecoder;
import com.btcode.netty.server.handler.AbstractJobIntercepter;
import com.btcode.netty.server.handler.HrtSessionCtrl;
import com.btcode.netty.server.session.IoSession;
import com.btcode.netty.server.session.SessionManager;
import com.btcode.netty.server.session.util.ChannelUtil;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.handler.timeout.IdleStateHandler;

public final class HrtNettyServer {
    private ILog log;
    private final int port;
    private EventLoopGroup bossGroup = new NioEventLoopGroup(1);
    private EventLoopGroup workerGroup = new NioEventLoopGroup();
    private ServerBootstrap b = new ServerBootstrap();
    private ChannelFuture f;

    /**
     * @param port                   netty对外服务端口
     * @param idleTimeSecond         离线检测时间，如果为0，则不监测
     * @param abstractHrtDecoder     自定义的消息解码器(由于需要多个下端共享使用，只能采用Class方式)，需继承自@AbstractHrtDecoder
     *                               自带会话管理功能，默认不开启，按要求实现getMnFromChannel使连接能唯一绑定即可
     * @param abstractJobIntercepter 自定义的业务数据处理器(由于需要多个下端共享使用，只能采用Class方式)，需继承自@AbstractJobIntercepter
     */
    public HrtNettyServer(int port, int idleTimeSecond, Class<? extends AbstractHrtDecoder> abstractHrtDecoder, Class<? extends AbstractJobIntercepter> abstractJobIntercepter) {
        log = MyLog.getInstance().getLogger(HrtNettyServer.class);
        this.port = port;

        //-Dio.netty.leakDetectionLevel=paranoid
        //-Dio.netty.maxDirectMemory=62914560
        //-XX:+PrintGCDateStamps
        //-XX:+PrintGCDetails
        //-Xloggc:./gclogs.gc


        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            try {
                if (f != null && f.channel() != null) {
                    f.channel().close().sync();
                }
                workerGroup.shutdownGracefully();
                bossGroup.shutdownGracefully();
            }
            catch (InterruptedException e) {
                log.error("", e);
            }
            log.info("销毁Netty服务完成");
        }));

        b.group(bossGroup, workerGroup)
            .channel(NioServerSocketChannel.class)
            .childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
//                    ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                    if (idleTimeSecond > 0) {
                        ch.pipeline().addLast(new MyIdleStateHandler(idleTimeSecond));
                    }
                    //不能直接传对象，要确保msgDecoderWithSessionCtrl和msgJobWithIntercepter继承相应的类 ChannelHandlerAdapter.ensureNotSharable
                    AbstractHrtDecoder ahd = abstractHrtDecoder.newInstance();
                    ch.pipeline().addLast(ahd).addLast(new HrtSessionCtrl().setDecoderRef(ahd)).addLast((abstractJobIntercepter.newInstance()));
                }
            })
            //详细参数说明： https://blog.csdn.net/hbtj_1216/article/details/74199162

//            .option(ChannelOption.ALLOCATOR, ByteBufAllocator.DEFAULT)
            // BACKLOG用于构造服务端套接字ServerSocket对象，标识当服务器请求处理线程全满时，用于临时存放已完成三次握手的请求的队列的最大长度。如果未设置或所设置的值小于1，Java将使用默认值50。
            .option(ChannelOption.SO_BACKLOG, 1024)
            //ChanneOption.SO_REUSEADDR对应于套接字选项中的SO_REUSEADDR，这个参数表示允许重复使用本地地址和端口。
            //比如，某个服务器进程占用了TCP的80端口进行监听，此时再次监听该端口就会返回错误，使用该参数就可以解决问题，该参数允许共用该端口，这个在服务器程序中比较常使用。
            //比如某个进程非正常退出，该程序占用的端口可能要被占用一段时间才能允许其他进程使用，而且程序死掉以后，内核一需要一定的时间才能够释放此端口，不设置SO_REUSEADDR就无法正常使用该端口。
            .option(ChannelOption.SO_REUSEADDR, true)
            //Channeloption.SO_KEEPALIVE参数对应于套接字选项中的SO_KEEPALIVE，该参数用于设置TCP连接，当设置该选项以后，连接会测试链接的状态，这个选项用于可能长时间没有数据交流的连接。
            //当设置该选项以后，如果在两小时内没有数据的通信时，TCP会自动发送一个活动探测数据报文。
            .childOption(ChannelOption.SO_KEEPALIVE, true);//维持链接的活跃，清除死链接
//                    .childOption(ChannelOption.SO_RCVBUF, true); //接收缓冲区大小
//                    .childOption(ChannelOption.SO_LINGER, true); //确保在close前发送完数据
        //ChannelOption.TCP_NODELAY参数对应于套接字选项中的TCP_NODELAY,该参数的使用与Nagle算法有关。
        //Nagle算法是将小的数据包组装为更大的帧然后进行发送，而不是输入一次发送一次，因此在数据包不足的时候会等待其他数据的到来，组装成大的数据包进行发送，虽然该算法有效提高了网络的有效负载，但是却造成了延时。
        //而该参数的作用就是禁止使用Nagle算法，使用于小数据即时传输。和TCP_NODELAY相对应的是TCP_CORK，该选项是需要等到发送的数据量最大的时候，一次性发送数据，适用于文件传输。
//                    .childOption(ChannelOption.TCP_NODELAY, true);
    }

    public void syncStart() {
        try {
            f = b.bind(port).sync();
        }
        catch (InterruptedException e) {
            log.error("Netty绑定端口异常", e);
        }
    }

    public void close() {
        try {
            f.channel().closeFuture().sync();
            log.info("Netty正常关闭");
        }
        catch (Exception e) {
            log.error("Netty异常关闭", e);
        }
        finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }

    class MyIdleStateHandler extends IdleStateHandler {
        MyIdleStateHandler(int readerIdleTimeSeconds) {
            super(readerIdleTimeSeconds, readerIdleTimeSeconds, 0);
        }

        @Override
        protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent e) throws Exception {
            super.channelIdle(ctx, e);
            if (e.state() == IdleState.READER_IDLE) {
                StringBuilder sb = new StringBuilder("会话离线超时，准备移除");
                IoSession session = SessionManager.INSTANCE.getSessionByChannel(ctx.channel());
                if (session != null) {
                    sb.append(session);
                }
                log.info(sb);
                ctx.channel().close();
            }
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            super.exceptionCaught(ctx, cause);
            log.error(ChannelUtil.getIp(ctx.channel()), cause);
        }
    }
}